/*
 * Created: Oct 14, 2007 2:08:10 PM
 * Original Name: UnMarshalFromXMLInterceptor.java
 * Creator: Matt
 */
package org.apache.struts2.interceptor.jaxb;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.Interceptor;

/**
 * <p>This Interceptor was created to patch a need I had for a specific
 *  project. This next revision will provide much more flexibility which
 *  should allow a much more variable usage and make it more realistic
 *  for others to use.</p>
 * <h2>Usage</h2>
 * <p>The standard usage is through the 2 parameters via querystring or form post.
 *  The default value of these parameters is "packageContext" and "xml". The
 *  packageContext attribute is "optional" as an attribute (it must still be
 *  somehow defined).</p>
 * <p>The packageContext can be defined a number of ways.<ol>
 * <li>Directly on the Interceptor &gt;param name="packageContext" value="com.mysite.blah"&lt;</li> 
 * <li>As a request parameter. Query string blah.action?packageContext=com.mysite.blah or as a form parameter.</li>
 * <li>As an attribute at the root of the XML you are posting. &gt;page packageContext="com.mysite.blah"&lt; </li>
 * </ol></p>
 * <p>If you set the straigthXML attribute to true you can post XML directly
 *  to the action. The package context will not be looked for as a request parameter
 *  parameter (obviously). So you must define it one of the other 4 methods.</p>
 *  
 * @author Matt Filion (www.mattfilion.com)
 *
 */
public class JAXBInterceptor implements Interceptor {

    /**
     * Default Serialization Id (1L)
     */
    private static final long               serialVersionUID            = 1L;

    protected static final Log              log                         = LogFactory.getLog(JAXBInterceptor.class);

    private static Map<String, JAXBContext> jaxbContexts                = new HashMap<String, JAXBContext>();

    private String                          packageContextParameterName = "jaxbPackageContext";
    private String                          xmlParameterName            = "jaxbXML";

    private String                          packageContext              = null;
    private boolean                         straightXML                 = false;
    private boolean                         putPackageContextInStack    = false;

    @Override
    public void init() {}

    @Override
    public void destroy() {}
    
    /**
     * 
     */
    public JAXBInterceptor() {
    } 
    
    /**
     * Attempts to return an already created JAXBContext.
     * @param contextPath
     */
    public static JAXBContext findJAXBContext(String contextPath){
        return jaxbContexts.get(contextPath);
    }

    /* (non-Javadoc)
     * @see com.opensymphony.xwork2.interceptor.AbstractInterceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
     */
    @Override
    public String intercept(ActionInvocation invocation) throws Exception{
        String xml = getXML(invocation);
        
        if(xml!=null){
            String packageContext = getPackageContext(invocation,xml);
    
            if(packageContext!=null){
                try {
                    Object object = getObject(packageContext,xml);
                    setObjectOnAction(object,invocation);
                } catch (JAXBException jaxbE) {
                    log.error("Could not create an object from the packageContext " + packageContext + " and xml "+xml,jaxbE);
                } catch (InvocationTargetException itE) {
                    throw itE;
                }
            }
        }

        return invocation.invoke();
    }
    
    /**
     * This method creates an object from the packgeContext and xml provided.
     * 
     * @param packageContext
     * @param xml
     * @return
     * @throws JAXBException
     */
    private Object getObject(String packageContext, String xml) throws JAXBException{
        JAXBContext  jaxbContext = getJAXBContext( packageContext );
        
        if(jaxbContext==null){
            return null;
        }

        // Get the xml representation of the objectToBeMarshalled
        Unmarshaller unMarshaller = jaxbContext.createUnmarshaller();
        return unMarshaller.unmarshal(new StringReader(xml));
    }

    /**
     * Sets the object on the action defined in the invocation
     * @param object
     * @param invocation
     * @throws InvocationTargetException
     */
    private void setObjectOnAction(Object object, ActionInvocation invocation) throws InvocationTargetException{

        if(object==null){
            log.warn("No object was created using the provided XML and package context");
            return;
        }

        Object action = invocation.getAction();

        if(action==null){
            log.info("There is no action for this ActionInvocation " + invocation.getInvocationContext().getName());
            return;
        }

        try {
            Method setMethod = action.getClass().getMethod("set"+object.getClass().getSimpleName(), object.getClass());
            setMethod.invoke(action, object);
        } catch (NoSuchMethodException nsmE){          
            log.warn("This interceptor was expecting a method called set"+object.getClass().getSimpleName() + " on the target action " + action.getClass().getName(), nsmE);
        } catch (IllegalArgumentException iargE){
            log.warn("The argument passed to the " +object.getClass().getSimpleName() + " was incorrect. The type passed was " + object.getClass().getName(),iargE);
        } catch (IllegalAccessException iaccE){
            log.warn("The method " +object.getClass().getSimpleName() + " has an invalid access type, the method may be protected or private instead of public. It could not be used to set the object created from the provided xml.", iaccE);
        }
    }
    

    /**
     * Retrieves the xml
     * @param invocation
     * @return
     */
    @SuppressWarnings("unchecked")
    private String getXML(ActionInvocation invocation){

        ActionContext actionContext = invocation.getInvocationContext();
        String        xml           = null;

        if(straightXML){
            Set<String> keySet = actionContext.getParameters().keySet();

            if(keySet.size()!=0){
                xml = keySet.iterator().next();

                if(xml==null){
                    log.info("The XML could not be located, straightXML attribute is set to true, so its expected as the very next value after the action definition on the querystring. (actionName.action?<xml....</xml>).");
                }
            } else {
                log.debug("No XML was found as a parameter.");
            }

        } else {
            xml = (String) actionContext.getParameters().get(xmlParameterName);

            if(xml==null){
                log.info("Could not locate any XML to UnMarshall an object from. Attempted using the parameter " + xmlParameterName);
            }
        }

        return xml;
    }

    /**
     * Retrieves the packageContext
     * @param invocation
     * @return
     */
    private String getPackageContext(ActionInvocation invocation, String xml){

        //If the package context is not set directly on the interceptor.
        if(packageContext==null){
            if(straightXML && xml!=null){
                try {
                    packageContext = getPackageContextFromXML(xml);
                } catch (ParserConfigurationException pcE) {
                    log.error(pcE);
                } catch (SAXException saxE) {
                    log.error(saxE);
                } catch (IOException ioE) {
                    log.error(ioE);
                }
            } else {
                //In most cases the package context will exist in the request.
                ActionContext actionContext = invocation.getInvocationContext();
                packageContext = (String) actionContext.getParameters().get(packageContextParameterName);

                //Search the XML for the package context, or try the lookup based on the XML name.
                if(packageContext==null){
                    try {
                        packageContext = getPackageContextFromXML(xml);
                    } catch (ParserConfigurationException pcE) {
                        log.error(pcE);
                    } catch (SAXException saxE) {
                        log.error(saxE);
                    } catch (IOException ioE) {
                        log.error(ioE);
                    }
                }
            }
        }

        if(packageContext==null){
            log.info("The JAXBInterceptor did not locate a package context so no UnMarshalling could take place.");
        } else if(putPackageContextInStack){
            log.info("Putting the package context into the stack, " +packageContext);
            invocation.getStack().set("_packageContext", packageContext);
        }

        return packageContext;
    }
    
    /**
     * 
     * @param invocation
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     */
    private String getPackageContextFromXML(String xml) throws ParserConfigurationException, SAXException, IOException{
        if(xml != null && xml.trim().equals("")==false){

            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            
            if(documentBuilderFactory!=null){
                
                documentBuilderFactory.setIgnoringComments(true);
                documentBuilderFactory.setIgnoringElementContentWhitespace(true);
                documentBuilderFactory.setValidating(false);
                
                DocumentBuilder documentBuilder    = documentBuilderFactory.newDocumentBuilder();
                if(documentBuilder!=null){
                    Document        document           = documentBuilder.parse(new ByteArrayInputStream(xml.getBytes()));
                    if(document!=null){
                        Node            rootNode           = document.getFirstChild();
                        NamedNodeMap    attributes         = rootNode.getAttributes();
                        Node            packageContextNode = attributes.getNamedItem("packageContext");
                      
                        if(packageContextNode!=null){
                            return packageContextNode.getFirstChild().getNodeValue();
                        } else {
                            return null;
                        }
                    }
                }
            }
        }
        return null;
    }
    
    /**
     * 
     * @param contextPath
     */
    private JAXBContext getJAXBContext(String contextPath){

        if(contextPath==null){
            return null;
        }

        JAXBContext jaxbContext = jaxbContexts.get(contextPath);
        
        if(jaxbContext==null){
            try {
                jaxbContext = JAXBContext.newInstance(contextPath);
                jaxbContexts.put(contextPath, jaxbContext);
            } catch (JAXBException e) {
                log.error("There was a problem while creating the JAXBContext");
            }
        }
        
        return jaxbContext;
    }

    /**
     * @param packageContextParameterName the packageContextParameterName to set
     */
    public void setPackageContextParameterName(String packageContextParameterName) {
        this.packageContextParameterName = packageContextParameterName;
    }

    /**
     * @param xmlParameterName the xmlParameterName to set
     */
    public void setXmlParameterName(String xmlParameterName) {
        this.xmlParameterName = xmlParameterName;
    }

    /**
     * @param packageContext the packageContext to set
     */
    public void setPackageContext(String packageContext) {
        this.packageContext = packageContext;
    }

    /**
     * @param straightXML the straightXML to set
     */
    public void setStraightXML(boolean straightXML) {
        this.straightXML = straightXML;
    }

    /**
     * @param putPackageContextInStack the putPackageContextInStack to set
     */
    public void setPutPackageContextInStack(boolean putPackageContextInStack) {
        this.putPackageContextInStack = putPackageContextInStack;
    }
}
